Computers arrange data into organized structures, so that the data can be easily located and accessed. One type of commonly-used data structure is the tree structure. With this structure, related pieces of data form individual nodes in the tree. Each node (except for the root node) will have only a single parent node, but may have a plurality of sibling nodes and a plurality of child nodes. Conventionally, a node A is referred to as a descendant of node B if node A's parent is node B, or if node A's parent is a descendant of node B. Similarly, node A is referred to as an ancestor of node B if node B is a descendant of node A.
FIG. 1 graphically illustrates how a tree structure can be used to organize information.
More particularly, this figure illustrates how a tree structure can be used to organize data relating to electronic ink, so that the ink can be manipulated by a user or recognized by a recognition function of an application. Electronic ink may be made up of strokes, with each stroke corresponding to, for example, movement of a pointing device. Each stroke includes information defining the properties of the stroke, such as the data points making up the stroke, a directional vector of the stroke, a color of the stroke, and a thickness at which the stroke is to be rendered on a display.
While strokes can be individually manipulated, it generally is more efficient to first organize strokes before manipulating them. Thus, a parser may be used to establish relationships between individual strokes, and then organize the strokes into larger units for editing or handwriting recognition. For example, a parser may be used to associate groups of related strokes together into units that form a word. Similarly, the parser may associate groups of one or more words together to form a line, and associate groups of one or more lines together to form a block or paragraph. The parser may then associate groups of one or more blocks or paragraphs together to form a single page or a document.
A parser typically will need to analyze electronic ink several times to produce a tree structure that accurately represents the relationships between the electronic ink strokes. Moreover, each time that the electronic ink is edited, the parser will need to update the tree. The parser may therefore need to operate frequently and for prolonged periods of time. To avoid having the parser constantly interfere with active software applications each time that it needs to refine the tree structure, the parser may instead continuously operate in the background with some environments.
FIG. 1 illustrates a tree structure 101 representing the results that might typically be provided by a parser. The tree 101 includes word nodes 103. Each word node 103 contains the data for the individual strokes that make up a corresponding word W. More particularly, if the parser has determined that a group of strokes makes up a word W, then the data for those strokes are contained (or reference by) the word node 103 representing the word W.
If multiple words W are associated by the parser with a single line L, then the word nodes 103 for the words W are arranged as children of a line node 105 corresponding to the line L. The line nodes 105 may include data common to all of its children, such as the color or thickness of the ink making up the words W in the line L. Line nodes 105, corresponding to lines L that the parser has associated into a block B, are then arranged as children of a block node 107 corresponding to the block B. The block nodes 107 in turn serve as children of a page node 109, which, in the illustrated example, is the root node for the tree 101. Of course, if the parser recognized multiple page boundaries, then the page node 109 might itself be a child of a root node corresponding to the entire document.
A number of different program threads may seek to concurrently access the information provided in the tree 101. For example, if a user is editing the electronic ink with a notetaking application, then the notetaking application will employ a user interface thread that changes the organization of the tree 101 to correspond with the user's edits. Thus, the user interface thread will attempt to execute read or write operations on one more nodes of the tree 101. On the other hand, the notetaking application will also employ a parser thread that may be continually refining the structure of the tree 101 in the background, as noted above. The parser thread may thus also attempt to execute a read or write operation on one or more nodes of the tree 101 at the same time as the user interface thread. Of course, other software applications may also employ threads that could concurrently attempt to access one more nodes of the tree 101 for various reasons.
Moreover, even a single software thread may attempt to sequentially execute one or more read or write operations on one more nodes of the tree 101. For example, in order to move a word W to a line L, the user interface thread may need to execute a read operation on the line node 105 corresponding to the line L, and execute a write operation on the subtree formed by the word node 103 corresponding to the word W.
As will be appreciated by those of ordinary skill in the art, it would be very undesirable to allow different threads to concurrently execute conflicting read or write operations on the same node. Accordingly, a thread seeking to access a node of a data structure must first initiate a “lock” on that node, to prevent a conflicting read or write operation of another thread from being executed on that node before its own read or write operation is complete. While the use of locks prevents conflicting read or write operations from concurrently executing on the same node, it creates new problems that can potentially stop the operation of the computer.
For example, referring to FIG. 2, a user interface thread may act to move a word W corresponding to the subtree 201 into the line L represented by the line node 203, as graphically illustrated by the dotted line 205. To complete this task, the user interface thread must request a write lock on the subtree 215. The user interface thread would then also request a write lock on the subtree 207 (that is, the subtree that includes the line node 203). Similarly, the parser thread may act to move the word W corresponding to the subtree 209 into the line L represented by the line node 211, as graphically illustrated by the dotted line 213. In order to complete its task, the parser thread would request a write lock on the subtree 207, and request another write lock on the subtree 215 (that is, the subtree that includes the line node 211).
A problem arises if, for example, the user interface thread obtains a write lock on the subtree 215, but cannot obtain a write lock on the subtree 207 before the parser thread obtains a write lock on the subtree 207. In this situation, the user interface thread will wait for access to the subtree 207 until the parser thread's lock on the subtree 207 is lifted. The parser thread, however, will maintain its write lock on the subtree 207 until it can acquire a write lock on the subtree 215. Because the user interface thread will maintain its lock on the subtree 215 until it can also obtain a lock on the subtree 207, both the user interface thread and the parser thread will reach a deadlock. That is, neither the user interface thread nor the parser thread will be able to complete its task until the other finishes. This situation will effectively stop the operation of both the user thread and the parser thread, and may even impact the operation of other software applications being run by the computer.
One solution to this problem is to allow a single software thread to obtain a lock on the entire data structure. Thus, the user interface thread would be able to obtain a lock on the entire tree 101. The user interface thread could then execute read and write operations as necessary, without interference from other threads. While this solution avoids the problem of deadlocks between different threads, it reduces the performance of other operations requiring access to the data structure. That is, allowing only one thread to use the data structure at any given time unnecessarily delays the operation of other threads that need the information in the data structure. For example, if the parser thread obtains a lock to the entire tree 101 in order to access the subtree 201, then the user interface thread may not simultaneously access the subtree 217, even though accessing the subtree 217 would not interfere with the parser thread's access to the subtree 201. Instead, the user interface thread must first wait for the parser thread to release the lock on the entire tree 101 before it can access the subtree 217, which may substantially delay the operation of the user interface thread.
Another solution to avoid deadlock is to allow a thread executing a write operation to obtain a lock on the entire data structure, while permitting different threads executing read operations to obtain concurrent locks. With this arrangement, a thread attempting to execute a write operation must either wait until all currently executing read operations are completed, or preempt (that is, prematurely end) the executing read operations. Thus, this solution also unnecessarily reduces the performance of operations requiring access to the data structure.
In addition to avoiding unnecessary performance reduction, it may actually be desirable to allow multiple threads to concurrently execute both read and write operations on a data structure. For example, as noted above, it may be useful to have the parser thread invisibly operate as a background process, even while the user is employing the user interface thread to manipulate the electronic ink. If the parser thread cannot execute both read and write operations on the tree 101 concurrently with, for example, the user interface thread, then the parser thread may noticeably prevent or delay the user interface thread from executing write operations.
It thus would be desirable to have a locking system that prevents deadlocks from occurring between different threads, but which does not unnecessarily reduce the performance of those threads. More particularly, it would be desirable to have a locking system for a data structure that allows different threads to concurrently obtain locks on different nodes of the data structure for both read and write operations.